Skip to main content

Ultrasonic

Guide to setup and use the ultrasonic sensor HC-SR04 with STM32CubeIDE

What it is for

This module is used to measure distances between 3 cm and 2 meters (otherwise it loses accuracy)

Constitution

The HC-SR04 ultrasonic module is equiped with two main parts, an ultrasonic transmitter, and an ultrasonic receiver. It contains 4 pins :

  • 5V pin to power the module
  • Trigger pin to control the transmitter
  • Echo pin to read the receiver
  • GND pin

Ultrasonic sensor

How it works

The ultrasonic transmitter sends periodic 8 cycle bursts of ultrasound at 40 kHz with boosted echo. Upon detecting an object, these waves are reflected towards the receiver. Therefore, the time it takes for these waves to travel to the object forth and back is used to calculate the distance.

Ultrasonic waves

Image credit : teachwithict

Visualization

In order to write the necessary STM code to correctly use this module, we have to understand what we should code precisely. There are mainly two things :

  • Pulse generation with the transmitter
  • Reading the echoed signal with the receiver

According to the datasheet, the trigger needs to be supplied by 10 μs pulses and then the transmitter will send out an 8 cycle burst of ultrasound at 40 kHz and raise its echo. The pulses sent to the Trigger pin should look like this:

pulses

zoomed in pulse

The signal read on the Echo pin looks like the following :

echo

In fact, the Echo signal stays on HIGH level until it receives the ultrasonic wave echo. Therefore, in order to calculate distance with its input, we should calculate the distance an ultrasonic wave travels during the time where it stays high, divided by two, because the wave travels the distance to the object back and forth.

In the following sections, we'll see how to do the right setup to finally calculate distance.

Pin setup

  • 5V pin should be connected to a 5V power supply (can be drawn from the STM)
  • Trigger pin should be connected to the STM in GPIO Output mode
  • Echo pin should be connected to the STM in GPIO_EXTI mode (will be explained later)
  • The GND pin should be connected to the GND node of the circuit

Pulse generation

In order to generate pulses, we are going to use some STM timer action. We need pulses with 10 μs width, we'll choose a periodicity of 200 ms in able to safely calculate distances up to 20 meters (you can do the maths yourself!). To generate these pulses, we will set a timer that creates interrupts every 10 μs. For our example, we're using an STML476RG microcontroller which has a core clock speed of 80 MHz, we choose to use the timer TIM2 for this mission. Go to the ioc configuration file and set the following values in the TIM2 section :

  • Set the Clock Source to Internal Clock

  • Set the PSC to 79

  • Set the ARR to 9

    These PSC and ARR values are to be adjusted to your core clock speed

    Save the ioc file and the necessary initialisation code will be generated by the IDE.

TIM2 config

Screenshot of the timer TIM2 settings

After setting up the timer, we should enable its interrupts, in the ioc file you go to Pinout&Confiration -> System Core -> NVIC and you check the TIM2 global interrupt box :

image

Screenshot of the NVIC settings

Now that the timer is set up, we can start generating the pulses, a pulse is simply a high level between two interruptions, every 200 ms. Therefore, the code is very simple, in the stm32*_it.c file (stm32l4xx_it.c in our example), you should find a function with the void TIM2_IRQHandler(void) prototype, this function is called every time the timer creates an interrupt, therefore every 10 μs. In 200 ms, there are 20.000 intervals of 10 μs width, that's why we define a constant called Trigger_Period = 20000. During the first one of these intervals, we want the signal sent to the Trigger pin to be set as HIGH, during the remaining intervals, we want it set to low, therefore comes the following code :

  void TIM2_IRQHandler(void)
{
/* USER CODE BEGIN TIM2_IRQn 0 */
if (trigger_counter == Trigger_Period - 1){ // Trigger_Period = 20000
HAL_GPIO_WritePin(Trigger_Port, Trigger_PIN, SET);
}
else if (trigger_counter == Trigger_Period){
HAL_GPIO_WritePin(Trigger_Port, Trigger_PIN, RESET);
trigger_counter = 0;
}

trigger_counter += 1;

/* USER CODE END TIM2_IRQn 0 */
HAL_TIM_IRQHandler(&htim2);
/* USER CODE BEGIN TIM2_IRQn 1 */

/* USER CODE END TIM2_IRQn 1 */
}

What's left is to start the timer after its initialization in the main.c file, you simply write HAL_TIM_Base_Start_IT(&htim2); in the USER BEGIN CODE 2 section of the main function.

You can visualise the output of the Trigger pin on the oscilloscope to verify that the pulse generation is correct.

Distance calculation

The distance calculation is done through the measuring of the time interval between sending the ultrasonic signal and receiving it back. Once the sonic burst is launched from the module, the Echo pin goes to HIGH state, and once it receives the signal back, it goes back to LOW state, as visualised before. So in order to calculate the distance, we have to detect this HIGH level using our microcontroller. A simple yet effective way to do it, is to detect the rising edge of the signal, and its falling edge, and calculating the time between them. Fortunately, the ST microcontroller comes with edge detection features. We mentioned earlier that the Echo pin should be configured in EXTI mode, this is because this mode allows the pin to do interruptions when certain events occur, EXTI actually stands for "external interrupt". In our case, we want interrupts to happen when a rising or a falling edge is detected, in order to check time on each of these instances. For our example, we configure the ECHO pin in GPIO_EXTI1 mode. Now we should configure the EXTI behavior. Go to the ioc file, Pinout&Confiration -> System Core -> GPIO and select the EXTI pin, select the External Interrupt with Rising/Falling edge triger detection option, make sure that the GPIO Pull-up/Pull-down is set to no.

echo pin

Screenshot of the GPIO settings

For measuring time, the calssic HAL_GetTick() function won't do the trick in our case, because it has a precision of 1 ms, and sounds travel around 35 cm in 1 ms! So it will generate a great imprecision when calculating distances. Instead, we'll set up a timer for getting the exact time on two instants : rising edge and falling edge. For our example, we set TIM1 with a high ARR value for high precision, and to have a time interval large enough to cover distances around 20 m, here is our configuration :

timer echo

Now we get to coding, we create two functions, one for handling the rising edge, it simply tracks the moment the rising edge occured, and the other function is for handling the falling edge, it tracks the moment the falling edge occured and it calculates the distance. You might have already figured out that the use of the timer can cause the second time to be lower than the first time because of the timer reset, that's why we use the ternary expression displayed in the code, it corrects the time shift. The high ARR value assures that the start time won't need to be shifted by more than one period.

uint32_t time_start = 0;
uint32_t time_end = 0;
float distance_cm = 0.0f;
extern TIM_HandleTypeDef htim1;


// to track instants, we use the counting of timer 1
void handle_rising_edge(void)
{
time_start = __HAL_TIM_GET_COUNTER(&htim1); // tracking of the instant of the rising edge
}

void handle_falling_edge(void)
{
time_end = __HAL_TIM_GET_COUNTER(&htim1); // tracking of the instant of the falling edge

uint32_t duration = (time_end >= time_start) ?
(time_end - time_start) :
((0xFFFF - time_start) + time_end); // the ternary expression assuring the right time calculation
// speed of an ultrasonic wave in the air ≈ 343 m/s = 0.0343 cm/µs
// the wave travels the distance back and forth - division by 2
distance_cm = ((float)duration * 0.0343f) / 2.0f;
}

Finally these functions should be called upon interruptions, in the stm32*_it.c or the main.c file, call them in the EXTI Callback function (you may need to define this function by yourself), according to the signal state.

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
if (GPIO_Pin == Echo_PIN)
{
if (HAL_GPIO_ReadPin(Echo_Port, Echo_PIN) == GPIO_PIN_SET)
{
handle_rising_edge();

}

else
{
handle_falling_edge();
}
}

}

Use

As you may have noticed, the constantly update variable "distance_cm" is declared as global, therefore you can use it anywhere in the project, you should simply call it in the .c file you're going to use it in, by typing extern float distance_cm; .

I suggest you try to apply that by writing a simple code that makes a LED light up when an object is detected at 10 cm. Otherwise you can try to print the constantly updated values of the distance in a terminal.